Journaux liées à cette note :

Journal du jeudi 09 janvier 2025 à 13:13 #iteration, #devlog, #capacitor, #POC

Nouvelle #iteration sur le Projet 17 - Créer un POC de création d'une app smartphone avec Capacitor.

Je viens de push le commit feat(android): implemented webview and configured deeplinks. J'ai passé en tout, 11 heures sur cette itération.

Je souhaite, dans cette note de type DevLog, présenter les difficultés et les erreurs rencontrées dans cette itération.


Étape 1 : mise en place d'un dummy website totalement statique

The Capacitor application in this POC displays the content of a demonstration website, with the HTML content located in the ./dummy-website/ folder.
This website is served by an HTTP Nginx server, launched using docker-compose.yml.

source

Pour faire très simple, j'ai choisi de créer un faux site totalement statique, qui sera affiché dans une webview de l'application smartphone.

Ce site contient juste 2 pages HTML ; celui-ci est exposé par un serveur HTTP nginx, lancé via un docker-compose.yml.


Étape 2 : Expose dummy website on Internet

En première étape, j'ai dû mettre en place une méthode pour facilement exposer sur Internet un dummy website lancé localement :

Expose dummy website on Internet

Why?
The Android and iOS emulators do not have direct and easy access to the HTTP service (dummy website) exposed on http://localhost:8080.

To overcome this issue, I use "cloudflared tunnel". You can also use other solutions, such as sish or ngrok Developer Preview. For more information, you can refer to the following note (in French): 2025-01-06_2105

source

Comme expliqué ci-dessus, cette contrainte est nécessaire afin de permettre à l'émulateur Android et à l'émulateur iOS (lancé sur une instance Scaleway Apple Silicon) aussi bien que sur mon smartphone physique personnel, d'accéder aux dummy website avec un support https.

Ceci était d'autant plus nécessaire, pour remplir les contraintes de configuration de la fonctionnalité Deep Linking with Universal and App Links.

C'est pour cela que j'ai dernièrement publié les notes suivantes : 2024-12-28_1621, 2024-12-28_1710, 2024-12-31_1853 et Alternatives managées à ngrok Developer Preview.

Pour simplifier la configuration de ce projet (poc-capacitor), j'ai décidé d'utiliser "cloudflared tunnel" en mode non connecté.

J'ai installé cloudflared avec Mise (voir la configuration ici).

Pour rendre plus pratique le lancement et l'arrêt du tunnel cloudflare, j'ai implémenté deux scripts :

Voici ce que cela donne à l'usage :

$ ./scripts/start-cloudflare-http-tunnel.sh
Starting the tunnel...
…wait… …wait… …wait…
Tunnel started successfully: https://moral-clause-interesting-broadway.trycloudflare.com

To stop the tunnel, you can execute:

$ ./scripts/stop-cloudflare-http-tunnel.sh
Stopping the tunnel (PID: 673143)...
Tunnel stopped successfully.

source


Étape 3 : configuration de la webview Capacitor

En réalité, par erreur, j'ai configuré la webview Capacitor après l'implémentation de la partie App links.

Au départ, je pensais qu'un simple window.location.href = process.env.START_URL; était suffisant pour afficher le site web dans l'application. En réalité, cette commande a pour effet d'ouvrir la page HTML dans le browser par défaut du smartphone. Je ne m'en étais pas tout de suite rendu compte.

Dans Capacitor, pour créer une webview dans une application, il est nécessaire l'utiliser la fonction InAppBrowser.openInWebView(... du package @capacitor/inappbrowser.

Voici l'implémentation dans le fichier /src/js/online-webview.js :

window.Capacitor.Plugins.InAppBrowser.openInWebView({
	url: startUrl,
	options: {
		// See https://github.com/ionic-team/capacitor-os-inappbrowser/blob/e5bee40e9b942da0d4dad872892f5e7007d87e75/src/defaults.ts#L33
		// Constant values are in https://github.com/ionic-team/capacitor-os-inappbrowser/blob/e5bee40e9b942da0d4dad872892f5e7007d87e75/src/definitions.ts
		showToolbar: false,
		showURL: false,

		clearCache: true,
		clearSessionCache: true,
		mediaPlaybackRequiresUserAction: false,

		// closeButtonText: 'Close',
		// toolbarPosition: 'TOP', // ToolbarPosition.TOP

		// showNavigationButtons: true,
		// leftToRight: false,
		customWebViewUserAgent: 'capacitor webview',

		android: {
			showTitle: false,
			hideToolbarOnScroll: false,
			viewStyle: 'BOTTOM_SHEET', // AndroidViewStyle.BOTTOM_SHEET

			startAnimation: 'FADE_IN', // AndroidAnimation.FADE_IN
			exitAnimation: 'FADE_OUT', // AndroidAnimation.FADE_OUT
			allowZoom: false
		},
		iOS: {
			closeButtonText: 'DONE',           // DismissStyle.DONE
			viewStyle: 'FULL_SCREEN',          // iOSViewStyle.FULL_SCREEN
			animationEffect: 'COVER_VERTICAL', // iOSAnimation.COVER_VERTICAL
			enableBarsCollapsing: true,
			enableReadersMode: false
		}
	}
});

source

Les paramètres dans options permettent de configurer la webview. J'ai choisi de désactiver un maximum de fonctionnalités.

En implémentant cette partie, j'ai rencontré trois difficultés :

  • Avec la version 1.0.2 du packages, j'ai rencontré ce bug "Bug- Android App crashing after adding this plugin, j'ai perdu presque 1 heure avant de le découvrir, pour fixer cela, j'ai choisi le QuickWin d'installer la version 1.0.1.
  • J'ai mis un peu de temps pour trouver les paramètres passés dans options
  • J'ai trouvé allowZoom: false pour supprimer l'affichage des boutons de zoom dans la webview

Étape 4 : setup de la partie App Links

Après avoir lu la page "Deep Linking with Universal and App Links", c'était la partie que je trouvais la plus difficile, mais en pratique, ce n'est pas très compliqué.

J'ai passé 5h30 sur cette partie, mais j'ai fait plusieurs erreurs.

Cette configuration se passe en 3 étapes.

Génération du fichier .well-known/assetlinks.json

La première consiste à générer le fichier le fichier dummy-website/.well-known/assetlinks.json qui est exposé par le serveur HTTP du dummy website.

Cette opération est documentée dans la partie "Create Site Association File".

Son contenu ressemble à ceci :

[
  {
    "relation": [
        "delegate_permission/common.handle_all_urls"
    ],
    "target": {
      "namespace": "android_app",
      "package_name": "$PACKAGE_NAME",
      "sha256_cert_fingerprints": ["$SHA256_FINGERPRINT"]
    }
  }
]

Il a une fonction de sécurité, il permet d'éviter de créer des applications malveillantes qui s'ouvriraient automatiquement sur des URLs sans lien avec l'application.

Il permet de dire « l'URL de ce site web peut automatiquement ouvrir l'application $PACKAGE_NAME » qui est signée avec la clé publique $SHA256_FINGERPRINT.

J'ai implémenté le script /scripts/generate-dev-assetlinks.sh qui permet automatiquement de générer ce fichier.

Lorsque j'ai travaillé sur cette partie, j'ai fait l'erreur de générer un certificat (voir le script /scripts/generate-dev-assetlinks.sh). Or, ce n'est pas la bonne méthode en mode développement.
Par défaut, Android met à disposition un certificat de développement dans ${HOME}/.android/debug.keystore.

La commande suivante me permet d'extraire la clé publique :

SHA256_FINGERPRINT=$(keytool -list -v \
  -keystore "${HOME}/.android/debug.keystore" \
  -alias "androiddebugkey" \ # password par défaut
  -storepass "android" 2>/dev/null | grep "SHA256:" | awk '{print $2}')

source

Configuration de AndroidManifest.xml

Comme indiqué ici, voici les lignes que j'ai ajoutées dans /android/app/src/main/AndroidManifest.xml.tmpl :

<intent-filter android:autoVerify="true">
   <action android:name="android.intent.action.VIEW" />
   <category android:name="android.intent.category.DEFAULT" />
   <category android:name="android.intent.category.BROWSABLE" />
   <data android:scheme="https" />
   <data android:host="{{ .Env.ALLOW_NAVIGATION }}" />
</intent-filter>

source

Petite digression sur mon usage des templates dans ce projet.

J'utilise gomplate pour générer des fichiers dynamiquement à partir de 4 templates (.tmpl) et des variables d'environnement configurées entre autres dans .envrc.

La génération des fichiers se trouve ici :

gomplate -f capacitor.config.json.tmpl -o capacitor.config.json
gomplate -f android/app/build.gradle.tmpl -o android/app/build.gradle
gomplate -f android/app/src/main/AndroidManifest.xml.tmpl -o android/app/src/main/AndroidManifest.xml
gomplate -f android/app/src/main/strings.xml.tmpl -o android/app/src/main/res/values/strings.xml

source

Les principaux éléments dynamiques sont :

export APP_NAME=myapp
export PACKAGE_NAME="xyz.sklein.myapp"
export START_URL=$(cat .cloudflared_tunnel_url)
export ALLOW_NAVIGATION=$(echo "$START_URL" | sed -E 's#https://([^/]+).*#\1#')

START_URL contient l'URL publique générée par cloudflared tunnel qui change à chaque lancement du tunnel.

Support deep links via l'interception de l'événement appUrlOpen

Troisième étape de la configuration de App links.

let timeoutId = setTimeout(() => {
	openInWebView(process.env.START_URL);
}, 200);

window.Capacitor.Plugins.App.addListener('appUrlOpen', (event) => {
	clearTimeout(timeoutId);
	openInWebView(event.url);
});

source

Cela permet d'implémenter la fonction deep links. Exemple : si l'utilisateur du smartphone clique sur l'URL https://dummysite/deep/ alors l'application va directement s'ouvrir sur la page /deep/ du dummy website.

Commandes utiles

La commande suivante permet de demander à l'OS Android de lancer une nouvelle "vérification" du fichier dummy-website/.well-known/assetlinks.json :

$ adb shell pm verify-app-links --re-verify ${PACKAGE_NAME}

source

Note : le fichier .cloudflared_tunnel_url contient l'URL du tunnel cloudflare qui expose le dummy website.

La commande suivante permet d'afficher la configuration actuelle App Link d'une application :

$ adb shell pm get-app-links ${PACKAGE_NAME}
  xyz.sklein.myapp:
    ID: 100ba7e3-b978-49ac-926c-8e6ec6810f5c
    Signatures: [AF:AE:25:7F:ED:98:49:A3:E0:23:B3:BE:92:08:84:A5:82:D1:80:AA:E0:A4:A3:D3:A0:E2:18:D6:70:05:67:ED]
    Domain verification state:
      association-pending-belt-acute.trycloudflare.com: verified

source

La commande suivante permet de tester le lancement de l'application à partir d'une URL passée en paramètre :

$ adb shell am start -W -a android.intent.action.VIEW -d "$(cat .cloudflared_tunnel_url)"
Starting: Intent { act=android.intent.action.VIEW dat=https://sc-lo-welsh-injury.trycloudflare.com/... }
Status: ok
LaunchState: COLD
Activity: xyz.sklein.myapp/.MainActivity
TotalTime: 1258
WaitTime: 1266
Complete

source

Cela fonctionne aussi avec une sous-page, par exemple : "$(cat .cloudflared_tunnel_url)/deep/?query=foobar".

Dans l'émulateur, Chrome ne lance pas les App Link !

Je pense que ce piège m'a fait perdre 2 h (sur les 5 h passées sur cette implémentation) !

Si j'ouvre l'URL du dummy website dans Chrome, l'application n'est pas lancée.

Mais, si j'ouvre l'URL dans l'application qui se nomme "Google", celle accessible via la barre de recherche en bas ce ce screenshot, l'App Link est bien pris en compte.

Problème : je testais mon application seulement dans Chrome. Et la fonctionnalité App Links ne fonctionnait pas. C'est seulement quand j'ai installé l'application sur mon smartphone physique personnel que j'ai constaté que App Links fonctionnait sous "Firefox Android".

J'ai constaté aussi que sur mon smartphone, Chrome n'ouvrait aucune application sur les URLs youtube.com, reddit.com, github.com

D'après ce que je pense avoir compris, la liste des applications qui peuvent ouvrir les App Links est listée dans la section "Settings => Apps => Default apps" :

J'ai fait des expériences sur 3 différents smartphones Android d'amis et à ce jour, je n'ai pas encore compris comment cela fonctionne. J'ai l'impression que c'est lié au browser par défaut configuré, mais j'ai trouvé des exceptions.

En tout cas, ce piège m'a fait perdre beaucoup de temps !


Note finale

Pour le moment, je n'ai pas eu besoin de configurer @capacitor/app-launcher, mais je pense que cela sera utile pour permettre à l'application d'ouvrir d'autres applications à partir d'une URL.

J'ai scripté pratiquement toutes les actions de ce projet.

ChatGPT m'a bien servi tout au long de cette implémentation.

Journal du jeudi 21 novembre 2024 à 17:36 #scaleway, #apple, #dev-kit, #iteration, #capacitor, #launch

Dans la note 2024-11-20_1102, je disais :

Prochaine étape du Projet 17 : Setup les iOS Requirements de Capacitor sur ce serveur Apple Silicon.

C'est chose faite 🙂.

Le repository poc-capacitor contient maintenant un script ./scripts/deploy-ios-requirements.sh qui permet d'exécuter ce script de provisioning sur le serveur Scaleway Apple Silicon distant : /provisioning/_ios.sh.

Ensuite, j'ai détaillé les étapes pour :

  • Uploader le projet sur le Scaleway Apple Silicon distant
  • Démarrer l'émulation d'un iPhone 15
  • Compiler et lancer l'application Capacitor dans l'émulateur iPhone 15
  • Et visualiser l'émulateur via VNC

Tout cela est détaillé ici : https://github.com/stephane-klein/poc-capacitor/tree/f109fb23dc612f486fad0d55ba939b4679841d06?tab=readme-ov-file#launch-application-on-ios

Je teste l'offre Scaleway Apple Silicon #scaleway, #apple, #dev-kit, #iteration

Dans le projet "Projet 17 - Créer un POC de création d'une app smartphone avec Capacitor" je disais :

Voici mon retour d'expérience d'utilisation de l'offre Scaleway Apple Silicon.

Voici la liste des images MacOS disponibles :

$ scw apple-silicon os list
ID                                    NAME                LABEL               IMAGE URL                                                                       FAMILY   IS BETA  VERSION  XCODE VERSION
59bf09f1-5584-469d-a0f6-55c8fee1ab81  macos-ventura-13.6  macOS Ventura 13.6  https://scw-apple-silicon.s3.fr-par.scw.cloud/scw-console/os/macos-ventura.png  Ventura  false    13.6     14
e08d1e5d-b4b9-402a-9f9a-97732d17e374  macos-sonoma-14.4   macOS Sonoma 14.4   https://scw-apple-silicon.s3.fr-par.scw.cloud/scw-console/os/macos-sonoma.png   Sonoma   false    14.4     15
7a8d85fb-781a-4212-8e47-240ec0c3d23f  macos-sequoia-15.0  macOS Sequoia 15.0  https://scw-apple-silicon.s3.fr-par.scw.cloud/scw-console/os/macos-sequoia.png  Sequoia  true     15.0     16

Voici la liste des types de serveurs disponibles dans la zone fr-par-3 :

$ SCW_DEFAULT_ZONE="fr-par-3" scw apple-silicon server-type list
Name  CPU                 Memory  Disk    Stock       Minimum Lease Duration
M1-M  Apple M1 (8 cores)  8.0 GB  256 GB  high stock  1 days

Et la liste dans la zone fr-par-1 :

$ SCW_DEFAULT_ZONE="fr-par-1" scw apple-silicon server-type list
Name  CPU                      Memory  Disk    Stock       Minimum Lease Duration
M2-M  Apple M2 (8 cores)       16 GB   256 GB  high stock  1 days
M2-L  Apple M2 Pro (10 cores)  16 GB   512 GB  high stock  1 days

Je souhaite installer un serveur de type M1-M à 0,11 € HT / heure, soit 2,64 € HT / jour, 80,3 € HT / mois.

Lors de ma première tentative, j'ai essayé de créer un serveur avec la commande suivante :

$ scw apple-silicon server create name=capacitor zone=fr-par-3 "$SCW_PROJECT_ID" M1-M 7a8d85fb-781a-4212-8e47-240ec0c3d23f
Invalid argument '46ad009f-xxxxxx': arg name must only contain lowercase letters, numbers or dashes

Suite à cette erreur, j'ai créé l'issue siuvante : Reduce"scw apple-silicon server create" helper message ambiguity.

$ scw apple-silicon server create name=capacitor project-id=${SCW_PROJECT_ID}  type=M1-M os-id=7a8d85fb-781a-4212-8e47-240ec0c3d23f zone=fr-par-3

Mais l'OS n'était pas trouvé, je me suis rendu compte que cette image OS n'était pas disponible dans la zone fr-par-3.

$ SCW_DEFAULT_ZONE="fr-par-3" scw apple-silicon os list
ID                                    NAME                LABEL               IMAGE URL                                                                       FAMILY   IS BETA  VERSION  XCODE VERSION
59bf09f1-5584-469d-a0f6-55c8fee1ab81  macos-ventura-13.6  macOS Ventura 13.6  https://scw-apple-silicon.s3.fr-par.scw.cloud/scw-console/os/macos-ventura.png  Ventura  false    13.6     14
e08d1e5d-b4b9-402a-9f9a-97732d17e374  macos-sonoma-14.4   macOS Sonoma 14.4   https://scw-apple-silicon.s3.fr-par.scw.cloud/scw-console/os/macos-sonoma.png   Sonoma   false    14.4     15

Voici finalement la commande de création de serveur qui a fonctionné avec succès :

$ scw apple-silicon server create name=capacitor project-id=$SCW_PROJECT_ID  type=M1-M os-id=e08d1e5d-b4b9-402a-9f9a-97732d17e374 zone=fr-par-3
ID              bb34d8ef-6305-4104-801c-1cf1b6b0f99f
Type            M1-M
Name            capacitor
ProjectID       46ad009f-xxxx
OrganizationID  215d7434-xxxx
IP              51.xxx.xxx.xxx
VncURL          vnc://m1:xxxx@51.xxx.xxx.121:5900
SSHUsername     m1
SudoPassword    xxxxxxx
Os              ID            e08d1e5d-b4b9-402a-9f9a-97732d17e374
Name          macos-sonoma-14.4
Label         macOS Sonoma 14.4
ImageURL      https://scw-apple-silicon.s3.fr-par.scw.cloud/scw-console/os/macos-sonoma.png
Family        Sonoma
IsBeta        false
Version       14.4
XcodeVersion  15

CompatibleServerTypes:
[M1-M M2-M M2-L]
Status             starting
CreatedAt          now
UpdatedAt          now
DeletableAt        23 hours from now
DeletionScheduled  false
Zone               fr-par-3

Voici le serveur créé :

$ scw apple-silicon server list
ID                                    TYPE  NAME       PROJECT ID
bb34d8ef-6305-xxxxx  M1-M  capacitor  46ad009f-54bc-4125-xxxxxx

Le serveur est passé en ready après environ 1min.

$ scw apple-silicon server get bb34d8ef-6305-xxxxxxx
...

CompatibleServerTypes:
[M1-M M2-M M2-L]
Status             ready
...
DeletableAt        23 hours from now
DeletionScheduled  false
...

Je peux me connecter directement en ssh au serveur :

$ ssh m1@xxx.xxx.xx.xxx
Last login: Wed Nov 20 16:22:10 2024
m1@bb34d8ef-6305-4104-801c-1cf1b6b0f99f ~ % uname -a
Darwin bb34d8ef-6305-4104-801c-1cf1b6b0f99f 23.4.0 Darwin Kernel Version 23.4.0: Fri Mar 15 00:12:41 PDT 2024; root:xnu-10063.101.17~1/RELEASE_ARM64_T8103 arm64

Je peux aussi me connecter au serveur via VNC (lien vers la documentation à ce sujet).

Installation des dépendances sous Fedora :

$ sudo dnf install -y remmina remmina-plugins-vnc

J'utilise le client VNC nommé Remmina.

$ remmina -c vnc://m1:xxxxx@51.xxxx.xxx.xxxx:5900

Les paramètres vnc et le mot de passe de l'user m1 sont disponibles dans la sortie de :

$ scw apple-silicon server get bb34d8ef-6305-xxxxxxx -o json
{
  "id": "bb34d8ef-6305-4104-xxxx-xxxxxxxxx",
  ...
  "vnc_url": "vnc://m1:xxxx@xxx.xxx.xxx.xxx:5900",
  "ssh_username": "m1",
  "sudo_password": "bTgkdiVUs7yT",
  ...
}

Il est possible de coller le mot de passe via la fonctionnalité « Envoyer le contenu du presse-papiers comme une saisie au clavier » de Remmina :

Attention, la réinstallation d'un serveur Apple Silicon prend au moins 45min.

J'ai implémenté des scritps de déploiement d'un Apple Silicon dans le POC : poc-capacitor.

Prochaine étape du Projet 17 : Setup les iOS Requirements de Capacitor sur ce serveur Apple Silicon.